package wintermute

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/onflow/flow-go/model/flow"
)

// wintermuteTimeout corresponds to the timeout the wintermute orchestrator have to conduct the attack.
const wintermuteTimeout = 2 * time.Minute

type WintermuteTestSuite struct {
	Suite
}

func TestWintermuteAttackTestSuite(t *testing.T) {
	suite.Run(t, new(WintermuteTestSuite))
}

// TestWintermuteAttack is passing upon a successful wintermute attack is conducted, i.e., wintermute attack orchestrator corrupting an execution
// result, and then orchestrating its corrupted verification nodes to verify it. The test passes if enough result approvals for corrupted chunks
// are witnessed in the test network, as well as the block corresponding to the corrupted execution result is sealed.
func (w *WintermuteTestSuite) TestWintermuteAttack() {

	corruptedResult := w.waitForExecutionResultCorruption()
	victimBlock := w.BlockState.WaitForBlockById(w.T(), corruptedResult.BlockID)

	// waits for the execution receipt of victim block from both CORRUPTED execution nodes.
	receiptB1 := w.ReceiptState.WaitForReceiptFrom(w.T(), victimBlock.Header.ID(), w.corruptedEN1Id)
	w.T().Logf("receipt for victim block generated by execution node-1: %x result ID: %x\n", w.corruptedEN1Id, receiptB1.ExecutionResult.ID())
	receiptB2 := w.ReceiptState.WaitForReceiptFrom(w.T(), victimBlock.Header.ID(), w.corruptedEN2Id)
	w.T().Logf("receipt for victim block generated by execution node-2: %x result ID: %x\n", w.corruptedEN2Id, receiptB2.ExecutionResult.ID())

	// makes sure corrupted execution nodes generated the corrupted result for this victim block.
	require.Equal(w.T(), receiptB1.ExecutionResult.ID(), corruptedResult.ID())
	require.Equal(w.T(), receiptB2.ExecutionResult.ID(), corruptedResult.ID())

	// waits for at least 2 approvals for all chunks of corrupted result from corrupted verification nodes.
	for i := 0; i < len(corruptedResult.Chunks); i++ {
		w.ApprovalState.WaitForTotalApprovalsFrom(w.T(),
			w.corruptedVnIds,
			corruptedResult.ID(),
			uint64(i),
			2)
	}

	// waits until we seal a height equal to the victim block height
	w.BlockState.WaitForSealedHeight(w.T(), victimBlock.Header.Height)
	// then checks querying victim block by height returns the original victim block.
	blockByHeight, ok := w.BlockState.FinalizedHeight(victimBlock.Header.Height)
	require.True(w.T(), ok)
	require.Equal(w.T(), blockByHeight.Header.ID(), victimBlock.Header.ID())
}

// waitForExecutionResultCorruption waits within a timeout till wintermute orchestrator corrupts an execution result.
// It returns the corrupted execution result by wintermute orchestrator.
func (w *WintermuteTestSuite) waitForExecutionResultCorruption() *flow.ExecutionResult {
	var corruptedResult *flow.ExecutionResult

	require.Eventually(w.T(), func() bool {
		corrupted, original, conducted := w.Orchestrator.AttackState()
		if !conducted {
			w.T().Logf("pending wintermute orchestrator to conduct attack")
			return false
		}

		corruptedResult = &corrupted

		w.T().Logf("wintermute orchesterator conducted corruption, original result: %x, corrupted result: %x", original.ID(), corrupted.ID())
		return true
	}, wintermuteTimeout, 100*time.Millisecond,
		fmt.Sprintf("orchestrator could not conduct execution result corruption within %v seconds", wintermuteTimeout))

	return corruptedResult
}
